import { New2DArray, New1DArray } from '../util'

export interface SudokuCeilValue {
  mainValue: number
  // candidates: 9 length array of bool
  candidates: boolean[]
  // the value color
  color: string
}

interface PuzzleCeilValue {
  mainValue: number
}

interface ColorCeilValue {
  color: string
  // if true, ignore the ceil
  ignored: boolean
}

interface AnswerCeilValue {
  mainValue: number
  // candidates: 9 length array of bool
  candidates: boolean[]
}

interface InsertParam {
  rowIdx: number
  colIdx: number
  value: number
}

interface DeleteParam {
  rowIdx: number
  colIdx: number
  value: number
}

/**
 * @brief deserialize string to sudokuGrid (2-d Array)
 * @param str: ecoded string like '1..2....3...', '.' means space
 * @param fn: callback fn, value is the parsed value, if ., then value is -1
 */
function stringToGrid<T>(str: string, fn: (value: number) => T): T[][] {
  if (str.length != 81) {
    throw new Error(`Expect str length 81, got: ${str.length}`)
  }

  const n = 9
  let result = New2DArray<T>(n, (rowIdx: number, colIdx: number) => {
    let value: number
    let strIdx = rowIdx * n + colIdx
    let ch = str.charAt(strIdx)

    if (ch === '.') {
      value = -1
    } else {
      value = parseInt(ch)
      if (!(value > 0 && value <= 9)) {
        throw new Error(`Invalid character: ${ch}`)
      }
    }

    return fn(value)
  })

  return result
}

function gridToString(grid: { mainValue: number }[][]): string {
  if (grid.length != 9) {
    throw new Error('Grid must be 9x9')
  }

  let str = ''
  grid.forEach(row => {
    row.forEach(val => {
      if (val.mainValue === -1) {
        str += '.'
        return
      }
      if (val.mainValue < 1 || val.mainValue > 9) {
        throw new Error(`Invalid value ${val.mainValue}`)
      }
      str += val.mainValue.toString()
    })
  })
  return str
}

export class Sudoku {
  private displayLayer: SudokuCeilValue[][]

  // Three layers from bottom to top
  // Puzzle -> Answer -> Color1 -> Color2...
  // All produce the displayLayer

  private puzzleLayer!: PuzzleCeilValue[][]
  private answerLayer!: AnswerCeilValue[][]
  private colorLayers!: Array<ColorCeilValue[][]>

  private layerOption = {
    puzzleColor: 'black',
    // answerColor: '#ed4014'
    answerColor: '#2d8cf0'
  }

  static NewDisplayState(): SudokuCeilValue[][] {
    const n = 9
    return New2DArray<SudokuCeilValue>(n, () => ({
      candidates: New1DArray(n, () => false),
      color: 'black',
      mainValue: -1
    }))
  }

  constructor(display: SudokuCeilValue[][]) {
    this.displayLayer = display
    this.resetLayers()
  }

  get displayState() {
    return this.displayLayer
  }

  colorLayerUpdated() {
    // update display layer
    this.updateDisplayerLayer((old, row, col) => {
      let newColor: string
      // If no color found in layer, set to default
      if (this.isPuzzleCeil(row, col)) {
        newColor = this.layerOption.puzzleColor
      } else {
        newColor = this.layerOption.answerColor
      }
      // find color first
      for (let i = this.colorLayers.length - 1; i >= 0; i--) {
        let { ignored, color } = this.colorLayers[i][row][col]
        if (ignored) {
          continue
        }
        newColor = color
      }

      return {
        mainValue: old.mainValue,
        candidates: old.candidates,
        color: newColor
      }
    })
  }

  puzzleLayerUpdated() {
    this.updateDisplayerLayer((old, row, col) => {
      let { mainValue } = this.puzzleLayer[row][col]
      if (mainValue == -1) {
        return old
      }
      // return the puzzle
      return {
        candidates: old.candidates,
        color: old.color,
        mainValue: mainValue
      }
    })
  }

  answerLayerUpdated() {
    this.updateDisplayerLayer((old, row, col) => {
      let { mainValue, candidates } = this.answerLayer[row][col]
      if (mainValue == -1) {
        return old
      }

      return {
        candidates: candidates,
        color: this.layerOption.answerColor,
        mainValue: mainValue
      }
    })
  }

  /**
   * clearAnswerLayer clear the answerLayer
   */
  clearAnswerLayer() {
    this.colorLayers = []
    this.answerLayer = New2DArray<AnswerCeilValue>(9, () => ({
      candidates: New1DArray(9, () => false),
      mainValue: -1
    }))

    this.updateDisplayerLayer((old, row, col) => {
      // return the puzzle
      return {
        candidates: New1DArray(9, () => false),
        changable: false,
        color: 'black',
        mainValue: -1
      }
    })
    this.puzzleLayerUpdated()
  }

  setPuzzleLayer(gridStr: string, puzzleColor: string) {
    this.layerOption.puzzleColor = puzzleColor
    // clear all answer layer and color layer
    this.resetLayers()

    this.puzzleLayer = stringToGrid<PuzzleCeilValue>(
      gridStr,
      (value: number) => ({
        mainValue: value
      })
    )
    this.puzzleLayerUpdated()
  }

  setAnswerLayer(gridStr: string) {
    this.clearAnswerLayer()

    this.answerLayer = stringToGrid<AnswerCeilValue>(
      gridStr,
      (value: number) => ({
        candidates: New1DArray(9, () => false),
        mainValue: value
      })
    )

    this.answerLayerUpdated()
  }

  addColorLayer(gridStr: string, color: string) {
    let layer = stringToGrid<ColorCeilValue>(gridStr, (value: number) => {
      if (value == -1) {
        return {
          color: color,
          ignored: true
        }
      }
      return {
        color: color,
        ignored: false
      }
    })

    this.colorLayers.push(layer)
    this.colorLayerUpdated()
  }

  popColorLayer() {
    this.colorLayers.pop()
    this.colorLayerUpdated()
  }

  changable(row: number, col: number) {
    return this.puzzleLayer[row][col].mainValue == -1
  }

  getAnswerLayerStr(): string {
    return gridToString(this.answerLayer)
  }

  /**
   *
   * @param param
   * @return number - old number
   */
  insertCertainNum(param: InsertParam): number {
    if (!this.changable(param.rowIdx, param.colIdx)) {
      return param.value
    }
    let answerCeil = this.answerLayer[param.rowIdx][param.colIdx]
    let oldNum = answerCeil.mainValue
    answerCeil.mainValue = param.value
    this.answerMainValueUpdated(param.rowIdx, param.colIdx)

    return oldNum
  }

  // !! TODO: Is inefficient if treat oldCeil
  // !! as Immutable object ?
  /**
   *
   * @param param
   * @return boolean - Insert Successfully ?
   */
  insertCandidateNum({ rowIdx, colIdx, value }: InsertParam): boolean {
    if (!this.changable(rowIdx, colIdx)) {
      return false
    }

    let answerCeil = this.answerLayer[rowIdx][colIdx]
    // ignore if already inserted
    if (answerCeil.candidates[value - 1] == true) {
      return false
    }
    // -1 let 1 to be 0
    answerCeil.candidates.splice(value - 1, 1, true)
    this.answerCandidateUpdated(rowIdx, colIdx, value)
    return true
  }

  removeCandidateNum({ colIdx, rowIdx, value }: DeleteParam): boolean {
    if (!this.changable(rowIdx, colIdx)) {
      return false
    }

    let answerCeil = this.answerLayer[rowIdx][colIdx]
    if (answerCeil.candidates[value - 1] == false) {
      // ignore if already deleted
      return false
    }
    answerCeil.candidates.splice(value - 1, 1, false)
    this.answerCandidateUpdated(rowIdx, colIdx, value)

    return true
  }

  removeCertainNum({ rowIdx, colIdx }: DeleteParam): number {
    if (!this.changable(rowIdx, colIdx)) {
      return -1
    }
    let answerCeil = this.answerLayer[rowIdx][colIdx]
    let oldValue = answerCeil.mainValue

    answerCeil.mainValue = -1
    this.answerMainValueUpdated(rowIdx, colIdx)

    return oldValue
  }

  private answerMainValueUpdated(rowIdx: number, colIdx: number) {
    let displayCeil = this.displayLayer[rowIdx][colIdx]
    let answerCeil = this.answerLayer[rowIdx][colIdx]

    displayCeil.candidates = answerCeil.candidates
    displayCeil.color = this.layerOption.answerColor
    displayCeil.mainValue = answerCeil.mainValue
  }

  private answerCandidateUpdated(
    rowIdx: number,
    colIdx: number,
    candidate: number
  ) {
    let displayCeil = this.displayLayer[rowIdx][colIdx]
    let answerCeil = this.answerLayer[rowIdx][colIdx]
    displayCeil.candidates.splice(
      candidate - 1,
      1,
      answerCeil.candidates[candidate - 1]
    )
  }

  private resetLayers() {
    const n = 9
    this.puzzleLayer = New2DArray<PuzzleCeilValue>(n, () => ({ mainValue: -1 }))
    this.answerLayer = New2DArray<AnswerCeilValue>(n, () => ({
      candidates: New1DArray(9, () => false),
      mainValue: -1
    }))
    this.colorLayers = []
  }

  private updateDisplayerLayer(
    fn: (
      oldValue: SudokuCeilValue,
      rowIdx: number,
      colIdx: number
    ) => SudokuCeilValue
  ) {
    this.displayLayer.forEach((row, rowIdx) => {
      row.forEach((value, colIdx) => {
        let newVal = fn(value, rowIdx, colIdx)
        row.splice(colIdx, 1, newVal)
      })
    })
  }

  private isPuzzleCeil(row: number, col: number): boolean {
    return this.puzzleLayer[row][col].mainValue != -1
  }
}
